# -*- coding: utf-8 -*-
import logging
import json
from datetime import datetime
from decimal import Decimal
import requests
from bson import json_util

from common.bankcard import db as bankcard_db
from common.bankcard import handler as bankcard_handler
from common.bankcard.model import BANKCARD_TYPE, BANKCARD_STATUS
from common.card_statistics_daily import db as card_statistics_daily_db
from common.mch import db as mch_db
from common.mch import handler as mch_handler
from common.order.handler import check_bank_card_daily_time
from common.order_withdraw import db as order_withdraw_db
from common.order_withdraw.db import ORDER_STATUS, NOTIFY_STATUS
from common.range import db as range_db
from common.utils.telegram import send
from common.utils import exceptions as err
from common.cache import redis_cache

_LOGGER = logging.getLogger('hydra')


def order_withdraw_finished(status):
    return status in [
        ORDER_STATUS.SUCCESS, ORDER_STATUS.MANUALSUCCESS, ORDER_STATUS.INVALID
    ]


def order_withdraw_not_finished(status):
    return status not in [
        ORDER_STATUS.SUCCESS, ORDER_STATUS.MANUALSUCCESS, ORDER_STATUS.INVALID
    ]


def order_withdraw_not_processing(status):
    return status not in [ORDER_STATUS.PROCESSING]


def notify(data):
    order_id = data['order_id']
    order = order_withdraw_db.get_order(order_id)

    if not order:
        raise err.ResourceNotFound('order_id ' + order_id + ' cannot be found')

    if order.status not in [
            ORDER_STATUS.SUCCESS, ORDER_STATUS.MANUALSUCCESS,
            ORDER_STATUS.INVALID
    ]:
        return

    bankcard = bankcard_db.get_bankcard_by_name(order.source_card_code)
    bankcard_balance = 0
    if bankcard:
        bankcard_balance = bankcard.balance

    notify_data = {
        'order_id': order.id,
        'mch_order_no': order.mch_order_no,
        'amount': "{0:,.2f}".format(order.amount),
        'status': ORDER_STATUS.get_key(order.status),
        'account_number': order.account_number,
        'card_balance': "{0:,.2f}".format(bankcard_balance),
        'type': 1
    }

    calculated_sign = mch_handler.generate_sign(order.mch_id, notify_data)
    notify_data['sign'] = calculated_sign

    response = requests.post(order.notify_url,
                             data=json.dumps(notify_data),
                             headers={'Content-Type': 'application/json'})

    response_text = response.text.upper(
    ) if response.status_code == 200 else 'FAILED'
    status = NOTIFY_STATUS.get_value(response_text)
    if not status:
        raise err.ResourceNotFound('notify response {} not found'.format(
            response.text.upper()))

    order_withdraw_db.push_order(order.id, status)


def auto_invalid():
    orders = order_withdraw_db.get_auto_invalid_orders()
    for order in orders:
        params = {}
        params['order_id'] = order.id
        params['status'] = ORDER_STATUS.INVALID
        params['notes'] = 'AUTO-INVALID'
        params['updated_by'] = "API"
        params['operating_time'] = None
        order = order_withdraw_db.edit_order(params)
        notify(params)


def auto_assign_new_card():
    orders = order_withdraw_db.get_auto_assign_new_card_orders()
    for order in orders:
        if order.status != ORDER_STATUS.PROCESSING:
            continue

        mch_account = mch_db.get_mch(order.mch_id)

        new_card, message, log_message = select_card_for_wirthdraw(
            mch_account.name, order.amount)
        if not new_card:
            no_card_error = 'mch_account.name {} error {}'.format(
                mch_account.name, message)
            no_card_error_log = 'mch_account.name {} error {}'.format(
                mch_account.name, log_message)
            _LOGGER.info('create order withdraw error: %s', no_card_error_log)
            raise err.ResourceNotFound(no_card_error)

        old_card_code = order.source_card_code

        params = {}
        params["source_account_name"] = new_card.account_holder
        params["source_account_number"] = new_card.account_number
        params["source_bank"] = new_card.bank
        params["source_card_code"] = new_card.name
        params["reassign_time"] = datetime.utcnow()
        params["amount"] = order.amount

        order_withdraw_db.upsert_order_withdraw(params,
                                                order.id,
                                                update_statistics=True)

        # Order Withdraw Reassign
        order_withdraw_db.create_order_withdraw_reassign(
            order.id, params["reassign_time"], old_card_code, new_card.name)


def retry_push_failed():
    try:
        orders = order_withdraw_db.get_notified_failed_orders()
        for order in orders:
            params = {}
            params['order_id'] = order.id
            notify(params)
    except Exception as e:
        _LOGGER.exception('order_withdraw retry_push_failed error %s', e)


def check_valid_order(params, status_func=None, msg=None):
    order_id = params['order_id']
    order = order_withdraw_db.get_order(order_id)

    if not order:
        raise err.ResourceNotFound('order_id {} cannot be found'.format(id))

    if order and status_func and status_func(order.status):
        error = '{} {}'.format(msg, ORDER_STATUS.get_key(
            order.status)) if msg else 'order_id {} is already {}'.format(
                order_id, ORDER_STATUS.get_key(order.status))
        raise err.DataError(error)

    return True


def select_card_for_wirthdraw(mch_name, amount):
    message = ''
    log_message = ''
    range_cards = filter(
        lambda r: r['type'] == 1 and r['status'] == 1 and mch_name.upper() in
        r['list_company'], range_db.get_range_for_orders())

    if not range_cards:
        range_json = json.dumps(range_db.get_range_for_orders(),
                                default=json_util.default)
        message = 'No Available Payment Range for Company {}.\r\n" + \
        "Either range status is disabled or company is not assigned.'.format(
            mch_name)
        log_message = '{}\r\nDetails {}'.format(message, range_json)
        return (None, message, log_message)

    cards = []

    bankcards = bankcard_db.get_bankcards()
    candidate_cards = filter(
        lambda b: redis_cache.get_bankcard_online_status(b.name) and Decimal(
            amount) <= b.balance and b.type == BANKCARD_TYPE.PAYMENT and b.status == BANKCARD_STATUS.ENABLE,
        bankcards)

    candidate_card_range = []
    for c in candidate_cards:
        if filter(
                lambda rc: c.name.upper() in rc['list_card'] and Decimal(
                    amount) >= Decimal(rc['range_from']) and Decimal(amount) <=
                Decimal(rc['range_to']), range_cards):
            candidate_card_range.append(c)

    if not candidate_card_range:
        range_json = [bc.as_dict() for bc in candidate_cards]
        range_json = json.dumps(DateTimeEncoder().encode(range_json))
        message = 'No Available Cards found in Range for Company {}.\r\n' + \
            'There are no cards suitable for this amount range.'.format(
                mch_name, range_json)
        log_message = '{}\r\nDetails {}'.format(message, range_json)
        return (None, message, log_message)

    for crd in candidate_card_range:
        today_stats = card_statistics_daily_db.get_cards_statistics(crd.name)
        if check_bank_card_daily_time(crd.opening_time, crd.closing_time):
            continue
        if today_stats.auto_close_count + today_stats.processing_count < crd.max_withdraw_order:
            cards.append(crd)

    selected_card = cards[0] if cards and len(cards) > 0 else None

    if selected_card:
        selected_card.withdraw_selected_at = datetime.utcnow()
        info = {}
        info['withdraw_selected_at'] = selected_card.withdraw_selected_at
        bankcard_db.upsert_bankcard(info, selected_card.id)
    else:
        message = 'No Card Available'

    return (selected_card, message, log_message)


def frequency_alert(card, host):
    daily = card_statistics_daily_db.get_cards_statistics(card.name)
    if daily.auto_close_count >= card.min_withdraw_order and \
       daily.auto_close_count <= card.max_withdraw_order:
        card_num = bankcard_handler.hide_account_number(card.account_number)
        telegram_header = "Bank Card Withdraw Success Limit Reached"
        message = "Bank: " + card.bank_en + "\r\n" + \
            " Card Code: " + card.name + "\r\n" + \
            " Username: " + unicode(card.account_holder) + "\r\n" + \
            " Account No: " + card_num + "\r\n" + \
            " Orders Closed: " + str(daily.auto_close_count) + "\r\n" + \
            " Maximum: " + str(card.max_withdraw_order)
        message = "*" + telegram_header + "* \r\n``` " + message + " ```"
        send(message, host, card.type)


class DateTimeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        if isinstance(obj, Decimal):
            return float(obj)

        return super(DateTimeEncoder, self).default(obj)
